feat(webapp): history can be narrowed by path, author and date (BEA-67) - #113
Merged
ssowonny merged 3 commits intoAug 3, 2026
Merged
Conversation
The project History view was a flat scroll with no controls but per-row restore/open/download. Fine at eleven rows; unreadable after a month of agent writes, and agents write far more than people do. Four reader filters on GET /api/p/<id>/history — q= (case-insensitive substring of the path), user= (exact account), since=/until= (UTC bounds, inclusive at both ends, RFC3339 or a bare YYYY-MM-DD). They compose with each other and with the existing path=/prefix= scoping, and they are applied in the same walk as path/prefix — BEFORE the sort and the cursor skip — so next_cursor keeps meaning "the next matching entry" and paging under a filter needed no new machinery. kinds[] is still computed over every op, so a filtered view classifies edits the same as the full feed. A malformed since/until is a 400, not a silently unfiltered feed. The filter bar drives those params through the URL rather than component state: a narrowed feed is a link you can send, it survives reload, and Back undoes it. The author list accumulates across fetches — rebuilding it from the current feed would strand a reader who filtered by one author with only their own name to pick from. Zero matches gets its own empty state with a Clear. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The escaped quotes inside the NewProjectDialog note made the whole classDiagram fail to parse — it has been rendering as an error box, not a diagram, since that note landed. Same breakage on origin/main; noticed while adding HistoryFilters to the same file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # internal/webapp/static/assets/index-C2ypx3jZ.js # internal/webapp/static/assets/index-CO632k9V.css # internal/webapp/static/index.html
ssowonny
deleted the
bea-67-ph-scan-bug-history-has-no-filter-search-date-range-or
branch
August 3, 2026 05:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
q/user/since/untilonGET /api/p/<id>/history— useful on its own, paging under a filter still walks every match.Before / after
Filtered, and the no-match state that offers a way back out:
?q=runbook?q=no-such-fileOn a phone the bar wraps to two rows and every control grows to a 44px tap target:
The API
GET /api/p/<id>/historytakes four new params, all optional, all composing with each other and with the existingpath=/prefix=scoping:q=user=op.User)since=YYYY-MM-DDread as a UTC dayuntil=The filters are applied in the same walk as
path/prefix, before the sort and before the cursor skip. That is the whole reason paging needed no new machinery:next_cursorkeeps meaning "the next matching entry". Filtering after the skip would silently drop rows, which is whatTestHistoryFilterPagingexists to catch.Two smaller decisions:
kinds[]is still computed over every op before any filter, so a filtered view never relabels an edit as an add.since > untilis not an error — it means "nothing", and returns nothing. A malformed date is a 400, matching the existinginvalid n/invalid cursorposture.The filter bar
Route.filters, parsed from the query string of the existing history route (/<project>/history[/<path>]?q=&user=&since=&until=). Navigation is a push, so Back undoes a filter like any other navigation.<select>is built from the accounts seen in the loaded feed and accumulates — rebuilding it from the current feed would leave a reader who filtered by one author with only that author to pick from, and "Anyone" as the only way out.YYYY-MM-DDand the server reads them as UTC days. The bar saysUTCout loud rather than converting silently, which would drop an evening's changes for anyone east of UTC.Deviation from the plan
The plan wanted the author control to also accept free text for an account not in the loaded window. Shipped as a plain
<select>instead: a select is discoverable where a datalist is not, and the accumulating list plus a hand-written?user=in the URL (which the bar then shows as the selection) covers the same ground without a second input. Say the word if you want the free-text field back.Architecture changes
architecture/webapp-frontend.md:routergainsRoute.filtersandhistoryFilterQuery/hasHistoryFilters, andurlForViewgains a fourthfiltersargument;componentsgainsHistoryFilters. No relationships changed.webapp-server.mdis untouched — it doesn't draw the history handler, and no Go type changed shape.Drive-by in the same file (second commit): the escaped quotes in the NewProjectDialog note broke the whole
classDiagram, sowebapp-frontend.mdhas been rendering as an error box rather than a diagram. Same onorigin/main; fixed here because it is the file this PR edits and the fix is one line.flowchart TB Browser["Browser"] router["<div style='text-align:left'><b>router</b><br/>+VIEW_ROUTES dashboard history install settings<br/>+LEGACY_VIEWS insights to dashboard<br/>+top-level routes orgs billing<br/>+parseRoute(url, mode) Route<br/>+Route.version ?v= sha, one past version<br/>+Route.trailingSlash notes/ resolves, then replaces to notes<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +Route.filters q user since until, history feed</span><br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +historyFilterQuery(filters) / hasHistoryFilters</span><br/>+urlForPath(path, projectId, version)<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +urlForView(view, projectId, target, filters) / encodePath / decodePath</span><br/><span style='background:#ef444455;padding:0 4px;border-radius:3px'>❌ <s>+urlForView / encodePath / decodePath</s></span></div>"] components["<div style='text-align:left'><b>components</b><br/>FileView FolderListing FileTree<br/>HistoryView HistoryRow <span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ HistoryFilters</span> DiffView VersionBanner<br/>Insights ShareDialog NewProjectDialog<br/>ShareBanner SharesTable AdminTable<br/>OrgAdmin HubSettings ProjectSettings<br/>Palette shell AccountBar ...</div>"] nav["nav"] Note["HistoryFilters drives the SERVER<br/>(?q= ?user= ?since= ?until=), never the loaded page:<br/>filtering what is on screen would lie about<br/>everything below the fold and break next_cursor.<br/>Its state is Route.filters, so a narrowed feed is<br/>linkable, survives reload, and Back undoes it."] Browser --> router Browser --> components components --> nav components -.- Note classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px classDef removed fill:#ef444422,stroke:#ef4444,stroke-width:2px,stroke-dasharray:4 3 classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2 class Note noteBoxTesting
go test ./...— all pass. New:TestHistoryFilters(each param alone, case-insensitivity, both-ends-inclusive dates, RFC3339 bounds, every composition, composition withpath=/prefix=),TestHistoryFilterPaging(n=2 + cursor walks the whole filtered set, no repeats, no gaps),TestHistoryBadDateRange(400, and no feed body).npm run e2e— 113 passed. Newe2e/history-filters.spec.tscovers filter → narrowed feed → reload keeps it → Back restores → Clear empties the query string, the no-match empty state, the author dropdown, and the folder/per-file shapes.npm test(router round-trip),go vet ./...,./check-dist.sh— clean.bob@x.ioon one file) so the author filter has something to exclude.Build session
(only works on the machine this ran on)